package main

import (
	"context"
	"crypto"
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/pem"
	"flag"
	"fmt"
	"io/ioutil"
	"os"
	"strings"

	"github.com/ipfs/go-log"

	logging "github.com/ipfs/go-log/v2"
	"github.com/libp2p/go-libp2p"
	crypto2 "github.com/libp2p/go-libp2p-core/crypto"
	"github.com/libp2p/go-libp2p-core/host"
	"github.com/libp2p/go-libp2p-core/network"
	"github.com/libp2p/go-libp2p-core/peer"
	"github.com/libp2p/go-libp2p-core/peerstore"
	"github.com/multiformats/go-multiaddr"
)

var logger = logging.Logger("main")

const ProtocolId = "HELLO"

func LoadOrCreateKey(keyPath string) (crypto2.PrivKey, error) {
	// 读取或创建二进制 key
	keyRaw, err := ioutil.ReadFile(keyPath)
	if err != nil {
		if os.IsNotExist(err) {
			// 生成 go 库的标准 key
			keyRsa, err := rsa.GenerateKey(rand.Reader, 2048)
			if err != nil {
				return nil, err
			}

			// 转换为二进制 key
			keyByte, err := x509.MarshalPKCS8PrivateKey(keyRsa)
			if err != nil {
				return nil, err
			}
			keyRaw = pem.EncodeToMemory(&pem.Block{
				Type:  "PRIVATE KEY",
				Bytes: keyByte,
			})

			// 写入文件
			err = ioutil.WriteFile(keyPath, keyRaw, 0o644)
			if err != nil {
				return nil, err
			}

			// 转换为 libp2p 的 key
			key, _, err := crypto2.KeyPairFromStdKey(keyRsa)
			if err != nil {
				return nil, err
			}
			return key, nil
		}
		return nil, err
	}
	block, _ := pem.Decode(keyRaw)

	// 转换为 go 库的标准 key
	var keyStd crypto.PrivateKey
	switch block.Type {
	case "PRIVATE KEY":
		keyStd, err = x509.ParsePKCS8PrivateKey(block.Bytes)
		if err != nil {
			return nil, err
		}
	default:
		return nil, fmt.Errorf("not support Private Key type: %s", block.Type)
	}

	// 转换为 libp2p 的 key
	key, _, err := crypto2.KeyPairFromStdKey(keyStd)
	if err != nil {
		return nil, err
	}

	return key, nil
}

func main() {
	keyPath := flag.String("key", "./peer.key", "key path")
	port := flag.Int("port", 5578, "listen tcp port")
	dest := flag.String("dest", "", "dest addr")
	flag.Parse()

	logging.SetAllLoggers(log.LevelDebug)

	key, err := LoadOrCreateKey(*keyPath)
	if err != nil {
		logger.Fatal(err)
	}

	h, err := libp2p.New(
		libp2p.Identity(key),
		libp2p.ListenAddrStrings(
			fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", *port),
			fmt.Sprintf("/ip6/::/tcp/%d", *port),
		),
	)
	if err != nil {
		logger.Fatal(err)
	}

	peerId := peer.Encode(h.ID())
	fmt.Printf("peer id: %s\n", peerId)
	for _, a := range h.Addrs() {
		fmt.Printf("%s/ipfs/%s\n", a, peerId)
	}

	if len(*dest) == 0 {
		serveMain(h)
	} else {
		clientMain(h, *dest)
	}
}

func serveMain(host host.Host) {
	host.SetStreamHandler(ProtocolId, helloHandler)
	<-make(chan struct{}) // hang forever
}

func clientMain(host host.Host, dest string) {
	// The following code extracts target's the peer ID from the
	// given multiaddress
	ipfsAddr, err := multiaddr.NewMultiaddr(dest)
	if err != nil {
		logger.Fatal(err)
	}
	pid, err := ipfsAddr.ValueForProtocol(multiaddr.P_IPFS)
	if err != nil {
		logger.Fatal(err)
	}

	peerIdServer, err := peer.Decode(pid)
	if err != nil {
		logger.Fatal(err)
	}

	// Decapsulate the /ipfs/<peerID> part from the target
	// /ip4/<a.b.c.d>/ipfs/<peer> becomes /ip4/<a.b.c.d>
	targetPeerAddr, _ := multiaddr.NewMultiaddr(
		fmt.Sprintf("/ipfs/%s", peer.Encode(peerIdServer)))
	targetAddr := ipfsAddr.Decapsulate(targetPeerAddr)

	// We have a peer ID and a targetAddr so we add
	// it to the peerstore so LibP2P knows how to contact it
	host.Peerstore().AddAddr(peerIdServer, targetAddr, peerstore.PermanentAddrTTL)

	stream, err := host.NewStream(context.Background(), peerIdServer, ProtocolId)
	if err != nil {
		logger.Fatal(err)
		return
	}
	defer func(stream network.Stream) {
		err := stream.Close()
		if err != nil {
			logger.Fatal(err)
		}
	}(stream)

	_, err = stream.Write([]byte("hello, I'm fkxxyz!"))
	if err != nil {
		logger.Fatal(err)
		return
	}
	err = stream.CloseWrite()
	if err != nil {
		logger.Fatal(err)
		return
	}

	recvData, err := ioutil.ReadAll(stream)
	if err != nil {
		logger.Fatal(err)
		return
	}
	fmt.Println("result: ", string(recvData))
}

func helloHandler(stream network.Stream) {
	defer func(stream network.Stream) {
		err := stream.Close()
		if err != nil {
			logger.Fatal(err)
		}
	}(stream)
	logger.Info("received stream")
	recvData, err := ioutil.ReadAll(stream)
	if err != nil {
		logger.Error(err)
		return
	}
	err = stream.CloseRead()
	if err != nil {
		logger.Error(err)
		return
	}
	logger.Infof("receive data: %s\n", recvData)
	data := []byte(strings.ToUpper(string(recvData)))
	_, err = stream.Write(data)
	if err != nil {
		logger.Error(err)
		return
	}
}
